package sh.grapendaal.tsuushin.module.uno.engine.impl;

import org.apache.commons.lang.NotImplementedException;

import sh.grapendaal.tsuushin.module.uno.engine.Input;
import sh.grapendaal.tsuushin.module.uno.engine.Output;
import sh.grapendaal.tsuushin.module.uno.engine.StatisticsTracker;
import sh.grapendaal.tsuushin.module.uno.engine.UnoEngine;
import sh.grapendaal.tsuushin.module.uno.engine.UnoRules;
import sh.grapendaal.tsuushin.module.uno.engine.domain.Card;
import sh.grapendaal.tsuushin.module.uno.engine.domain.Card.Rank;
import sh.grapendaal.tsuushin.module.uno.engine.domain.Playable;
import sh.grapendaal.tsuushin.module.uno.engine.domain.Playable.Type;
import sh.grapendaal.tsuushin.module.uno.engine.domain.Player;
import sh.grapendaal.tsuushin.module.uno.engine.dto.GameAction;
import sh.grapendaal.tsuushin.module.uno.engine.dto.GameState;
import sh.grapendaal.tsuushin.module.uno.engine.dto.helper.PlayerStateHelper;
import sh.grapendaal.tsuushin.module.uno.engine.event.CommandEvent;
import sh.grapendaal.tsuushin.module.uno.engine.event.dispatcher.GameEventDispatcher;
import sh.grapendaal.tsuushin.module.uno.engine.event.listener.EventListener;
import sh.grapendaal.tsuushin.module.uno.engine.service.AchievementService;
import sh.grapendaal.tsuushin.module.uno.engine.service.CardService;
import sh.grapendaal.tsuushin.module.uno.engine.service.PersistenceService;
import sh.grapendaal.tsuushin.module.uno.engine.service.PlayerService;

/**
 * This class will completely handle a single game of Uno. It keeps track of all
 * the participating players, the cards and game rules.
 */
public class UnoEngineImpl extends GameEventDispatcher implements UnoEngine, EventListener<CommandEvent> {
	
	private CardService cardService;
	private PlayerService playerService;
	private AchievementService achievementService;
	private PersistenceService persistenceService;
	private Input input;
	private Output output;
	
	// Stateful properties.
	private State state = State.IDLE;
	private StatisticsTracker statisticsTracker;
	private GameState gameState;

	UnoEngineImpl() {
		// Package private constructor. Only the factory and unit tests may
		// create the engine.
	}

	@Override
	public State getState() {
		return state;
	}

	@Override
	public Input getInput() {
		return input;
	}

	public void setInput(Input input) {
		this.input = input;
		this.input.setEngine(this);
		this.input.addListener(this.achievementService.getCommandEventListener());
		this.input.addListener(this);
	}

	@Override
	public Output getOutput() {
		return output;
	}

	public void setOutput(Output output) {
		this.output = output;
		addListener(this.output);
	}

	@Override
	public CardService getCardService() {
		return cardService;
	}

	public void setCardService(CardService cardService) {
		this.cardService = cardService;
	}

	public StatisticsTracker getStatisticsTracker() {
		return statisticsTracker;
	}

	public GameState getGameState() {
		return gameState;
	}

	public void setGameState(GameState gameState) {
		this.gameState = gameState;
	}

	@Override
	public PlayerService getPlayerService() {
		return playerService;
	}

	public void setPlayerService(PlayerService playerService) {
		this.playerService = playerService;
		addListener(this.playerService);
	}

	@Override
	public AchievementService getAchievementService() {
		return achievementService;
	}

	public void setAchievementService(AchievementService achievementService) {
		this.achievementService = achievementService;
		addListener(this.achievementService.getGameEventListener());
	}
	
	@Override
	public PersistenceService getPersistenceService() {
		return persistenceService;
	}
	
	public void setPersistenceService(PersistenceService persistenceService) {
		this.persistenceService = persistenceService;
	}

	private GameState createGameState() {
		GameState gameState = new GameState();
		gameState.setTopCard(this.getCardService().getTopCard());
		gameState.setPlayers(PlayerStateHelper.createFromPlayer(this.getPlayerService().getPlayers()));
		gameState.setCurrentPlayer(PlayerStateHelper.createFromPlayer(this.getPlayerService().getCurrentPlayer()));
		gameState.setNextPlayer(PlayerStateHelper.createFromPlayer(this.getPlayerService().peekNextPlayer()));
		return gameState;
	}

	@Override
	public void onPlayerJoin(CommandEvent event) {
		Player player = event.getPlayer();
		if (!this.getPlayerService().hasPlayer(player)) {
			Player loadedPlayer = this.getPersistenceService().loadPlayer(player.getNickname());
			if (loadedPlayer != null) {
				// This player has played Uno before.
				this.getPlayerService().addPlayer(loadedPlayer);
			} else {
				// A new player! How lovely.
				this.getPlayerService().addPlayer(player);
			}

			if (this.getState() == State.IDLE) {
				this.state = State.PENDING;
			}

			dispatchPlayerJoin(createGameState());
		}
	}

	@Override
	public void onPlayerLeave(CommandEvent event) {
		Player player = event.getPlayer();
		if (this.getPlayerService().hasPlayer(player)) {
			this.getPlayerService().removePlayer(player);
			dispatchPlayerLeave(createGameState());
		}
	}

	@Override
	public void onGameStart(CommandEvent event) {
		if (this.getPlayerService().getNumPlayers() < UnoRules.MINIMAL_PLAYERS_NEEDED) {
			this.getOutput().showError("At least two players are needed to play Uno.");
			return;
		}
		if (this.getStatisticsTracker() == null) {
			this.statisticsTracker = new StatisticsTrackerImpl();
			addListener(this.getStatisticsTracker());
			
			start();
		}
	}

	@Override
	public void onGameStop(CommandEvent event) {
		dispatchGameStop(createGameState());
		stop();
	}

	@Override
	public void onCardPlay(CommandEvent event) {
		Player player = event.getPlayer();
		Playable played = event.getPlayed();

		if (!player.isTurn()) {
			return;
		}

		if (played.getType() == Type.CARD) {
			Card playedCard = (Card) played;
			if (!player.hasCard(playedCard)) {
				this.getOutput().showError("You do not have that card.");
				return;
			}
			if (!this.getCardService().getTopCard().validateCard(playedCard)) {
				this.getOutput().showError("You tried to play an invalid card.");
				return;
			}
			if (player.getDrawnCard() != null && !playedCard.equals(player.getDrawnCard())) {
				this.getOutput().showError("You can only play the card you just drew.");
				return;
			}

			this.getCardService().switchTopCard(player.playCard(playedCard));
			
			dispatchCardPlay();

			if (UnoRules.hasWon(player.getCards())) {
				// This player won!
				dispatchGameEnd();
				stop();
			} else {
				// Check if we need to fuck over the next player in one way or
				// another.
				int advanceTurns = 1;
				Player nextPlayer = null;

				switch (played.getRank()) {
				case DRAW_TWO:
				case DRAW_FOUR:
					nextPlayer = this.getPlayerService().peekNextPlayer();
					int numCards = played.getRank() == Rank.DRAW_TWO ? 2 : 4;

					for (int i = 0; i < numCards; i++) {
						nextPlayer.addCard(this.cardService.getCardFromDeck(), false);
					}
					break;

				case REVERSE:
					if (this.getPlayerService().getNumPlayers() > 2) {
						this.getPlayerService().reverseRotation();
						break;
					}

					// Deliberate fall-through here because a reverse with only
					// 2 players is the same as a skip.

				case SKIP:
					advanceTurns = 2;
					break;
				}

				advanceTurn(advanceTurns);
			}
		} else {
			// Type == Stack
			throw new NotImplementedException();
		}
	}

	private void advanceTurn(int number) {
		dispatchTurnEnd();

		for (int i = 0; i < number; i++) {
			// Advance player.
			this.getPlayerService().nextPlayer();
			if (i < number - 1) {
				dispatchTurnSkip();
			}
		}

		dispatchTurnStart();
	}

	@Override
	public void onCardDraw(CommandEvent event) {
		Player player = event.getPlayer();
		if (!player.isTurn()) {
			return;
		}

		if (player.hasDrawnCard()) {
			this.getOutput().showError("You've already drawn a card. If you can't play it, pass your turn.");
			return;
		}

		Card drawnCard = this.getCardService().getCardFromDeck();
		player.addCard(drawnCard, true);

		dispatchCardDraw();
	}

	@Override
	public void onCardPass(CommandEvent event) {
		Player player = event.getPlayer();
		if (!player.isTurn()) {
			return;
		}

		if (!player.hasDrawnCard()) {
			this.getOutput().showError("You cannot pass your turn without drawing a card first.");
			return;
		}

		dispatchCardPass();
		advanceTurn(1);
	}

	/**
	 * Starts the game.
	 */
	private void start() {
		this.state = State.INITIALIZING;

		this.getCardService().initialize();
		deal();

		this.getPlayerService().initialize();
		for (Player player : this.playerService.getPlayers()) {
			this.getOutput().showCards(PlayerStateHelper.createFromPlayer(player));
		}

		this.state = State.PLAYING;
		dispatchGameStart();
		dispatchTurnStart();
	}

	private void stop() {
		for (Player player : this.getPlayerService().getPlayers()) {
			this.getPersistenceService().storePlayer(player);
		}
		this.getPersistenceService().storeGameReport(this.getStatisticsTracker().getGameReport());
		
		removeListener(this.output);

		// FIXME This breaks when doing !stop:
		// java.util.ConcurrentModificationException
		// this.input.removeListener(this.achievementService.getCommandEventListener());
		// this.input.removeListener(this);
		// this.input.setEngine(null);

		removeListener(this.achievementService.getGameEventListener());
		removeListener(this.statisticsTracker);
		removeListener(this.playerService);

		this.state = State.IDLE;

		this.cardService.destroy();

		this.output = null;
		this.input = null;
		this.achievementService = null;
		this.statisticsTracker = null;
		this.playerService = null;
		this.cardService = null;
	}

	/**
	 * Gives every player a bunch of cards and draws the first top card, to get
	 * the game going.
	 */
	private void deal() {
		// Done like this to mimic real life dealing situation.
		for (int i = 0; i < UnoRules.NUM_CARDS_PER_PLAYER; i++) {
			for (Player player : this.playerService.getPlayers()) {
				player.addCard(this.cardService.getCardFromDeck(), false);
			}
		}

		this.cardService.openTopCard();
	}

	private void dispatchGameStart() {
		this.gameState = createGameState();
		this.gameState.setLastAction(GameAction.GAME_STARTED);
		super.dispatchGameStart(this.gameState.clone());
	}

	private void dispatchGameEnd() {
		this.gameState.setLastAction(GameAction.GAME_ENDED);
		super.dispatchGameEnd(this.gameState.clone());
	}

	private void dispatchCardPlay() {
		this.gameState.setLastAction(GameAction.CARD_PLAYED);
		super.dispatchCardPlay(this.gameState.clone());
	}

	private void dispatchCardDraw() {
		this.gameState.setLastAction(GameAction.CARD_DRAWN);
		super.dispatchCardDraw(this.gameState.clone());
	}

	private void dispatchCardPass() {
		this.gameState.setLastAction(GameAction.CARD_PASSED);
		super.dispatchCardPass(this.gameState.clone());
	}

	private void dispatchTurnStart() {
		this.gameState = createGameState();
		this.gameState.setLastAction(GameAction.GAME_STARTED);
		super.dispatchTurnStart(this.gameState.clone());
	}

	private void dispatchTurnSkip() {
		this.gameState = createGameState();
		this.gameState.setLastAction(GameAction.TURN_SKIPPED);
		super.dispatchTurnSkip(this.gameState.clone());
	}

	private void dispatchTurnEnd() {
		this.gameState.setLastAction(GameAction.TURN_ENDED);
		super.dispatchTurnEnd(this.gameState.clone());
	}
}